# Copyright (c) 2009, RPI-ACM, Jesse Kempf <jessekempf@gmail.com>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# - Redistributions of source code must retain the above copyright notice,
#     this list of conditions and the following disclaimer.
# - Redistributions in binary form must reproduce the above copyright
#     notice, this list of conditions and the following disclaimer in the
#     documentation and/or other materials provided with the distribution.
# - Neither the names of RPI-ACM nor the names of its contributors
#     may be used to endorse or promote products derived from this software
#     without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
#  TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
#  PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
#  OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
#  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
#  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
#  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
#  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
#  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
#  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Scheduling tools for use in daemon main loops."""

import sys
import time
from collections import defaultdict

def job(timing, priority=0):
	"""Decorate a function with timing information and a priority so
	a Scheduler can use it."""
	def decorator(func):
		"""See above."""
		func.period = Scheduler.parse_timespec(timing)
		func.priority = priority
		return func
	return decorator

class Scheduler(object):
	"""A cooperative multitasking job scheduler.

	The following will check 10 times per second for jobs ready to be
	run and will print out "Hello, world!" twice a second.

	@job("2hz")
	def somejob():
		print "Hello, world!"

	s = Scheduler("10hz")
	s.schedule(somejob)

	s.run()

	Timings are specified as either frequencies or periods. A frequency is
	measured in hertz and is more convenient for jobs that must be executed
	more than once a second. Frequencies are indicated by strings that end 
	in "hz". A period is measured in seconds between invocations and is more
	convenient for scheduling jobs that must be separated by one or more
	seconds.  Periods are indicated by strings that end in "s".
	"""
	@staticmethod
	def parse_timespec(timing):
		"""Take a timing string, such as '25.4hz' or '31s' and convert
		it to a floating point number of seconds."""

		if timing.endswith('hz'):
			return float(1) / float(timing.rstrip('hz'))
		elif timing.endswith('s'):
			return float(timing.rstrip('s'))
		else:
			raise ValueError("Timing parameter must be in seconds or hertz.")

	def __init__(self, timing):
		"""Create a Scheduler with a fundamental clock rate given by the
		timing parameter."""
		self.tick = Scheduler.parse_timespec(timing)
		self.maxticks = sys.maxint
		self.jobqueue = defaultdict(list)

		self.state = "INIT"

	def secs_to_ticks(self, secs):
		"""Convert a number of seconds into the nearest number of
		corresponding clock ticks for this scheduler."""
		return int(float(secs) / self.tick)

	# pylint: disable-msg=W0142
	def run(self):
		"""Run all scheduled jobs."""
		self.state = "RUN"
		ticks = 0
		while self.state == "RUN":
			while len(self.jobqueue[ticks]):
				(func, fargs, fkwargs) = self.jobqueue[ticks].pop()
				func(*fargs, **fkwargs)
				next = ticks + self.secs_to_ticks(func.period)
				self._schedule(next, func, fargs, fkwargs)
			time.sleep(self.tick)
			ticks = (ticks + 1) % self.maxticks
			
	# pylint: enable-msg=W0142

	def stop(self):
		"""Stop the scheduling and running of jobs."""
		self.state = "STOP"

	def _schedule(self, ticks, func, fargs, fkwargs):
		"""Do the heavy lifting of adding a job to the queue and
			keeping the queue sorted."""
		self.jobqueue[ticks].append((func, fargs, fkwargs))
		self.jobqueue[ticks] = sorted(self.jobqueue[ticks],
			lambda x,y: -cmp(x[0].priority, y[0].priority))

	def schedule(self, sched_job, *fargs, **fkwargs):
		"""Schedule a job to be run and passed the given arguments."""
		ticks = self.secs_to_ticks(sched_job.period)
		if ticks == 0:
			raise ValueError("Job frequency can not be higher than clock rate.")
		self._schedule(ticks, sched_job, fargs, fkwargs)
